<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>子类</title>
</head>

<body>
  <script>
    // 9.5.1子类和原型
    // 父类Range的实例
    function Range(from, to) {
      this.from = from
      this.to = to
    }
    // 所有Range对象都继承这个对象，这个属性必须命名为prototype才行
    Range.prototype = {
      // 如果x在范围内则返回true，否则返回false
      // 这个方法适用于文本、日期和数值范围
      includes: function (x) { return this.from <= x && x <= this.to },
      // 这个生成器函数让这个类的实例可迭代
      // 注意：只适用于数值范围
      [Symbol.iterator]: function* () {
        for (let x = Math.ceil(this.from); x <= this.to; x++)yield x;
      },
      // 返回范围的字符串表示
      toString() { return "(" + this.from + "..." + this.to + ")" },
      constructor: Range
    }
    // 子类的构造函数
    function Span(start, span) {
      if (span >= 0) {
        this.from = start;
        this.to = start + span
      } else {
        this.to = start;
        this.to = start + span
      }
    }
    // 子类原型继承父类
    Span.prototype = Object.create(Range.prototype)
    // 不想继承Range.prototype.constructor
    // 因此需要定义自己的constructor属性
    Span.prototype.constructor = Span
    // Span定义自己的toString()方法，Span覆盖了toString（），否则就要从Range继承
    Span.prototype.toString = function () {
      return `(${this.from}...+${this.to - this.from})`
    }
    console.log(new Span(1, 3));
    console.log(new Span(1, 3).toString());

    // 9.5.2通过extends和super创建子类
    // Array的一个简单子类，为第一个和最后一个元素添加了获取函数
    class EZArray extends Array {
      get first() { return this[0] }
      get last() { return this[this.length - 1] }
    }
    let a = new EZArray()
    console.log(a);
    console.log(a instanceof EZArray);//a是子类的实例
    console.log(a instanceof Array);//a是父类的实例
    a.push(1, 2, 3, 4)//可以使用继承的方法
    console.log(a.pop());
    console.log(a.first);
    console.log(a.last);
    console.log(a[1]);
    console.log(Array.isArray(a));//子类实例确实是数组
    console.log(EZArray.isArray(a));//子类也继承了静态方法
    // 经过以上分析 EZArray的实例之所以能继承实例方法，是因为EZArray.prototype继承Array.prototype
    console.log(Array.prototype.isPrototypeOf(EZArray.prototype));
    // EZArray之所以能继承静态方法和属性，EZArray继承Array，是因为extends关键字独有的特性，在ES6之前是不可能做到的
    console.log(Array.isPrototypeOf(EZArray));

    // 示例：Map检查键和值类型的子类（TypedMap.js）
    class TypedMap extends Map {
      //私有字段如果传入entries会调入set，影响私有字段读取报错，因为调用自己的set的时候this还没有值，就像报错，尚未完成初始化
      // #keyType;
      // #valueType;
      constructor(keyType, valueType, entries) {
        // console.log(this);
        console.log(new.target);
        // 使用（通过类型检查的）初试条目初始化父类
        super(entries)
        // 然后初始化子类 ，保存键和值的类型
        this.keyType = keyType
        this.valueType = valueType
        // 如果指定了条目，检查它们的类型
        if (entries) {
          console.log(entries);
          for (let [k, v] of entries) {
            console.log(k);
            // console.log(typeof k);
            console.log(v);
            if (typeof k !== keyType || typeof v !== valueType) {
              throw new TypeError(`Wrong type for entry [${k},${v}]`)
            }
          }
        }
      }

      // 现在，重定义set（）方法，为所有新增映射条目添加类型检查逻辑
      set(key, value) {
        // 如果键或值的类型不对就抛出错误
        if (this.keyType && typeof key !== this.keyType) {
          // console.log(this.keyType);
          throw new TypeError(`${key} is not of type ${this.keyType}`)
        }
        if (this.valueType && typeof value !== this.valueType) {
          throw new TypeError(`${value} is not of type ${this.valueType}`)
        }
        // 如果类型正确，则调用超类的set（），方法为映射添加条目。同时，返回父类方法返回的值
        return super.set(key, value)
      }
    }
    let a1 = {
      name: 'wcy',
      age: 20
    }
    // [[a1.name, a1.age]]
    let b = new TypedMap('string', 'number', [[a1.name, a1.age]])
    console.log(b);
    console.log(b.set('谢霆锋', 40));

    // 9.5.3委托而不是继承
    /*
    *一个类似Set的类，但会记录值被添加的次数
    *可以像使用Set一样调用add()和remove（）
    *调用count()获取某个值已经被添加了多少次
    *默认迭代器回送至少被添加过1次的值。如果想迭代[value,count]对，使用entries()
    */
    class Histogram {
      // 初始化只涉及创建一个要委托的Map对象
      constructor() {
        this.map = new Map()
      }

      // 对给定的键，次数就是映射中的值
      // 如果映射中不存在这个键，则为0
      count(key) { return this.map.get(key) || 0 }

      // 这个类似Set的方法has()在次数大于0时返回true
      has(key) { return this.count(key) > 0 }

      // 直方图的大小就是映射中条目的数量
      get size() { return this.map.size }

      // 要添加一个键，只需递增其在映射中的次数
      add(key) { this.map.set(key, this.count(key) + 1) }

      // 删除键，必须在次数回到0时从映射中删除相应的键
      delete(key) {
        let count = this.count(key)
        if (count === 1) {
          this.map.delete(key)
        } else if (count > 1) {
          this.map.set(key, count - 1)
        }
      }

      // 迭代直方图就是返回映射中存储的键
      [Symbol.iterator]() { return this.map.keys() }
      // 其他迭代器方法直接委托给映射对象
      keys() { return this.map.keys() }
      values() { return this.map.values() }
      entries() { return this.map.entries() }
    }
    let c = new Histogram()
    console.log(c);
    console.log(c.add('name'));//添加一个键
    console.log(c.add('name'));
    console.log(c.add('name1'));
    console.log(c.count('name'));//2个name
    console.log(c.has('name'));//true
    console.log(c.delete('name'));//删除键
    console.log(c.count('name'));//1个
    console.log(c.size);//映射数量
    console.log(c.keys());//0: "name"，1: "name1"
    console.log(c.values());//0: 1，1: 1
    console.log([...c]);//0: {"name" => 1}，1: {"name1" => 1}

    // 9.5.4类层次与抽象类
  </script>
</body>

</html>